/*
 * UAE - The Un*x Amiga Emulator
 *
 * Emulate very basic Amiga-like environment for XFD slaves
 *
 * All this only because there is no portable XFD replacement..
 *
 * (c) 2007 Toni Wilen
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "sysconfig.h"
#include "sysdeps.h"
#include "zfile.h"
#include "fsdb.h"
#include "uae.h"
#include "custom.h"
#include "memory.h"
#include "newcpu.h"

#include "cpu_small.h"
#include "cputbl_small.h"

/* memory structure
 *
 * 0x000676 execbase
 * 0x000ffc current start of free memory
 * 0x001000 allocmem code (exec)
 * 0x001080 freemem code, dummy (exec)
 * 0x001f00 XFD buffer info
 * 0x002000 first XFD slave
 * 0x?????? next XFD slave
 * ....
 * 0x?????? decrunched data
 * 0x?????? free memory (available for slave)
 * 0xxxf000 end of decompression buffer
 * end      stack
 */

static uaecptr exec = 0x676;
static uaecptr freememaddr = 0x0ffc;
static uaecptr allocmem = 0x1000;
static uaecptr freemem = 0x1080;
static uaecptr bufferinfo = 0x1f00;
static uaecptr stacksize = 0x1000;
struct xfdslave
{
    struct xfdslave* next;
    uaecptr start;
    char* name;
};

static struct xfdslave* xfdslaves;

#define FAKEMEM_SIZE 524288

static uae_u32 gl(uae_u8* p)
{
    uae_u32 v;

    v = p[0] << 24;
    v |= p[1] << 16;
    v |= p[2] << 8;
    v |= p[3] << 0;
    return v;
}
static uae_u16 gw(uae_u8* p)
{
    uae_u16 v;

    v = p[0] << 8;
    v |= p[1] << 0;
    return v;
}
static void pl(uae_u8* p, uae_u32 v)
{
    p[0] = v >> 24;
    p[1] = v >> 16;
    p[2] = v >> 8;
    p[3] = v >> 0;
}

static uae_u8* codeptr, * xfdmemory;
static uaecptr codememory;
static uae_u32 xfdmem_mask;

static int load_xfd(char* path)
{
    struct zfile* z;
    uae_u8* buf, * p;
    int size;
    int first, last, numhunks;
    int hunksize;
    uae_u8* prevhunk;
    int i, j;
    uaecptr hunks[10];
    uaecptr startaddr;

    z = zfile_fopen(path, "rb");
    if (!z)
        return 0;
    zfile_fseek(z, 0, SEEK_END);
    size = zfile_ftell(z);
    zfile_fseek(z, 0, SEEK_SET);
    buf = xmalloc(size);
    zfile_fread(buf, size, 1, z);
    zfile_fclose(z);
    p = buf;
    if (gl(p) != 0x3f3)
        goto end;
    p += 4 + 8;
    first = gl(p);
    p += 4;
    last = gl(p);
    p += 4;
    numhunks = last - first + 1;
    prevhunk = 0;
    startaddr = codememory;

    for (i = 0; i < numhunks; i++)
    {
        uaecptr out = codememory;
        hunksize = gl(p) * 4;
        p += 4;
        pl(codeptr + out, hunksize);
        if (prevhunk)
        {
            pl(prevhunk, out);
            prevhunk = codeptr + out;
        }
        hunks[i] = out + 8;
        codememory += hunksize + 8;
    }
    for (i = 0; i < numhunks; i++)
    {
        uae_u32 htype = gl(p);
        uae_u32 hsize = gl(p + 4) * 4;
        uaecptr haddr = hunks[i];
        uaecptr srchunk;
        uae_u32 relocnum, relochunknum;

        p += 8;
        if (htype == 0x3e9 || htype == 0x3ea)
        {
            memcpy(codeptr + haddr, p, hsize);
            p += hsize;
        }
        else if (htype != 0x3eb)
        {
            write_log("RELOC: unknown hunk %08X\n", htype);
            goto end;
        }
        htype = gl(p);
        p += 4;
        if (htype == 0x3f2)
            continue;
        if (htype != 0x3ec)
        {
            write_log("RELOC: expected 000003EC but got %08X\n", htype);
            goto end;
        }
        relocnum = gl(p);
        p += 4;
        relochunknum = gl(p);
        p += 4;
        srchunk = hunks[relochunknum];
        for (j = 0; j < relocnum; j++)
        {
            uae_u32 off = gl(p);
            p += 4;
            pl(codeptr + haddr + off, gl(codeptr + haddr + off) + srchunk);
        }
    }
    write_log("XFD slave '%s' loaded and relocated @%08X (%d bytes) succesfully\n", path, startaddr, codememory - startaddr);
    p = codeptr + startaddr + 8;
    if (gl(p + 4) != 'XFDF')
    {
        write_log("XFD header corrupt\n");
        goto end;
    }
    p = codeptr + gl(p + 20);
    for (;;)
    {
        int version = gw(p + 4);
        int mversion = gw(p + 6);
        uaecptr name = gl(p + 8);
        int flags = gw(p + 12);
        int minsize = gl(p + 28);
        uae_u8* nameptr = codeptr + name;
        struct xfdslave* xfds;

        write_log("- '%s' ver %d, master ver %d, minsize %d\n",
                  nameptr, version, mversion, minsize);
        xfds = xcalloc(sizeof(struct xfdslave), 1);
        xfds->name = nameptr;
        xfds->start = p - codeptr;
        if (!xfdslaves)
        {
            xfdslaves = xfds;
        }
        else
        {
            struct xfdslave* x = xfdslaves;
            while (x->next)
                x = x->next;
            x->next = xfds;
        }
        if (!gl(p))
            break;
        p = codeptr + gl(p);
    }
    return 1;
end:
    return 0;
}

static void initexec(void)
{

    pl(codeptr + 4, exec);

    // fake memory allocation functions
    pl(codeptr + exec - 198, allocmem);
    pl(codeptr + exec - 210, freemem);

    // lea -4(pc),a0
    pl(codeptr + allocmem + 0, 0x41fafffa);
    // move.l (a0),d1; add.l d0,(a0)
    pl(codeptr + allocmem + 4, 0x2210d190);
    // move.l d1,d0; rts
    pl(codeptr + allocmem + 8, 0x20014e75);

    pl(codeptr + freemem, 0x4e750000); // rts
}

int init_xfd(void)
{
    static int init;
    char tmp[MAX_DPATH];
    void* d;

    if (init > 0)
        return 1;
    if (init < 0)
        return 0;
    init = -1;

    init_cpu_small();
    codememory = 0x2000;
    codeptr = malloc(FAKEMEM_SIZE);
    sprintf(tmp, "%splugins%cxfd", start_path_data, FSDB_DIR_SEPARATOR);
    d = my_opendir(tmp);
    if (d)
    {
        while (my_readdir(d, tmp))
        {
            char tmp2[MAX_DPATH];
            sprintf(tmp2, "%splugins%cxfd%c%s", start_path_data, FSDB_DIR_SEPARATOR, FSDB_DIR_SEPARATOR, tmp);
            load_xfd(tmp2);
        }
        my_closedir(d);
    }
    initexec();
    codeptr = realloc(codeptr, codememory);
    xfdmemory = malloc(FAKEMEM_SIZE);
    init = 1;
    return 1;
}

void xop_illg(uae_u32 opcode)
{
    write_log("minicpu illegal opcode %04.4x\n", opcode);
    xm68k_setpc(0);
}

static int execute68k(void)
{
    uaecptr stack = xm68k_areg(7);
    xm68k_areg(7) = stack - 4;
    for (;;)
    {
        uae_u32 pc;
        uae_u32 opcode = xget_iword(0);
        (*xcpufunctbl[opcode])(opcode);
        if (xm68k_areg(7) == stack)
            return 1;
        pc = xm68k_getpc();
        if (pc <= 0x100 || pc >= FAKEMEM_SIZE)
        {
            write_log("minicpu crash, pc=%x\n", pc);
            return 0;
        }
    }
}

static struct zfile * decomp(struct zfile* zf, struct xfdslave* xfds, uae_u32 size)
{
    uae_u8* p;
    uae_u32 decompsize;
    uaecptr decompaddr;
    struct zfile* zfout;

    p = xfdmemory + bufferinfo;
    memset(p, 0, 20 * 4);
    xregs.regs[8] = bufferinfo; // A0
    decompsize = gl(p + 16 * 4);
    if (decompsize <= 0)
        return 0;
    decompaddr = FAKEMEM_SIZE - stacksize - decompsize;
    pl(p + 6 * 4, decompaddr);  // TargetBuffer
    pl(p + 8 * 4, decompsize);
    if (!execute68k())
        return 0;
    if (!xregs.regs[0])
        return 0;
    decompsize = gl(p + 16 * 4);
    zfout = zfile_fopen_empty(zfile_getname(zf), decompsize);
    zfile_fwrite(xfdmemory + decompaddr, decompsize, 1, zf);
    return zfout;
}

uae_u32 xget_long(uaecptr addr)
{
    uae_u32* m;
    addr &= xfdmem_mask;
    m = (uae_u32*)(xfdmemory + addr);
    return do_get_mem_long(m);
}
uae_u32 xget_word(uaecptr addr)
{
    uae_u16* m;
    addr &= xfdmem_mask;
    m = (uae_u16*)(xfdmemory + addr);
    return do_get_mem_word(m);
}
uae_u32 xget_byte(uaecptr addr)
{
    addr &= xfdmem_mask;
    return xfdmemory[addr];
}
void xput_long(uaecptr addr, uae_u32 l)
{
    uae_u32* m;
    addr &= xfdmem_mask;
    m = (uae_u32*)(xfdmemory + addr);
    do_put_mem_long(m, l);
}
void xput_word(uaecptr addr, uae_u32 w)
{
    uae_u16* m;
    addr &= xfdmem_mask;
    m = (uae_u16*)(xfdmemory + addr);
    do_put_mem_word(m, w);
}
void xput_byte(uaecptr addr, uae_u32 b)
{
    addr &= xfdmem_mask;
    xfdmemory[addr] = b;
}

struct zfile * decompress_zfd(struct zfile* z)
{
    unsigned int size;
    uae_u8* p;
    struct xfdslave* xfds;
    struct zfile* zfout = z;

    if (!init_xfd())
        return zfout;
    memset(xfdmemory, 0, FAKEMEM_SIZE);
    memcpy(xfdmemory, codeptr, codememory);
    xfdmem_mask = FAKEMEM_SIZE - 1;

    p = xfdmemory + codememory;
    zfile_fseek(z, 0, SEEK_END);
    size = zfile_ftell(z);
    zfile_fseek(z, 0, SEEK_SET);
    zfile_fread(p, size, 1, z);

    xfds = xfdslaves;
    while (xfds)
    {
        uaecptr start = xfds->start;
        memset(&xregs, 0, sizeof xregs);
        pl(codeptr + freememaddr, codememory + size); // reset start of "free memory"
        xregs.regs[0] = size; // D0
        xregs.regs[8] = codememory; // A0
        xregs.regs[9] = bufferinfo; // A1
        xregs.regs[15] = FAKEMEM_SIZE; // A7
        pl(xfdmemory + bufferinfo + 0x00, codememory); // SourceBuffer
        pl(xfdmemory + bufferinfo + 0x04, size); // SourceBufLen
        xm68k_setpc(gl(xfdmemory + start + 16)); // recog code
        if (xregs.pc)
        {
            if (execute68k())
            {
                if (xregs.regs[0])
                {
                    write_log("XFD slave '%s' recognised the compressed data\n", xfds->name);
                    xm68k_setpc(gl(xfdmemory + start + 20)); // decomp code
                    if (xregs.pc)
                    {
                        struct zfile* zz = decomp(z, xfds, size);
                        if (zz)
                        {
                            zfout = zz;
                            break;
                        }
                    }
                }
            }
        }
        xfds = xfds->next;
    }

    return zfout;
}